Skip to content

feat(engine): add runtime Copilot model controls - #2246

Open
jamesadevine with Copilot wants to merge 24 commits into
mainfrom
copilot/agent-issue-mirror-runtime-model-controls
Open

jamesadevine with Copilot wants to merge 24 commits into
mainfrom
copilot/agent-issue-mirror-runtime-model-controls

Conversation

Copilot AI commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ADO operators could not switch Agent or Detection Copilot models during an outage without editing workflow markdown and recompiling. This adds runtime model controls through Azure DevOps YAML variables, UI variables, and variable groups while preserving explicit front-matter model precedence.

  • Runtime precedence
    • engine.model / effective Detection model
    • ADO_AW_MODEL_AGENT_COPILOT or ADO_AW_MODEL_DETECTION_COPILOT
    • ADO_AW_DEFAULT_MODEL_COPILOT
    • existing Copilot CLI default
variables:
  ADO_AW_MODEL_AGENT_COPILOT: gpt-5-mini
  ADO_AW_MODEL_DETECTION_COPILOT: claude-sonnet-4.5
  • Bundled Copilot invocation

    • replaces generated Copilot command/model Bash with a strict, versioned copilot-invoker.js protocol
    • constructs real argv arrays, resolves runtime models inside AWF, forwards signals, and preserves Copilot exit codes
    • writes a requested-model result before launch; trusted host code validates and merges it into aw_info.json
    • stages the version-matched ado-script.zip in every Agent and enabled Detection job through existing release/feed/pipeline-artifact supply-chain paths
  • Safe runtime plumbing

    • maps Azure DevOps model variables through typed step env entries, avoiding macro expansion inside generated Bash
    • resolves them when the Agent or Detection task starts, so preceding task.setvariable steps are honored
    • validates runtime model identifiers and sets Copilot CLI's native COPILOT_MODEL only in the child environment
    • rejects competing raw engine.args --model and engine.env.COPILOT_MODEL configuration
    • keeps explicit configured models static and authoritative
  • Accurate run metadata

    • records the requested Agent and Detection session models in aw_info.json
    • reports the OTel-observed Agent model separately when a custom-agent model declaration supersedes the requested model
    • treats unavailable Agent metadata as non-fatal so isolated runtime probes can still execute

Test plan

  • full cargo test
  • cargo clippy --all-targets -- -D warnings
  • cargo check --all-targets
  • cargo test --bin ado-aw compile::shell
  • cargo test --test generated_shell_guard
  • cargo test --test bash_lint_tests
  • npm --prefix scripts/ado-script test -- --pool=forks --fileParallelism=false
  • npm --prefix scripts/ado-script run typecheck
  • npm --prefix scripts/ado-script run build
  • npm --prefix scripts/ado-script run test:smoke
  • standalone, 1ES, job, and stage compiler regressions
  • real Linux AWF + Copilot + SafeOutputs contract
  • PR CI and ado-script E2E

@azure-pipelines

azure-pipelines Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI and others added 4 commits September 25, 2026 19:52
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Copilot AI changed the title [WIP] Add runtime model controls to ado-aw feat(engine): add runtime Copilot model controls Sep 25, 2026
Copilot AI requested a review from jamesadevine September 25, 2026 20:02
Comment thread src/engine.rs Outdated
\x20\x20\x20\x20*[!A-Za-z0-9._:-]*)\n\
\x20\x20\x20\x20\x20\x20echo \"ERROR: runtime Copilot model from {specific}/{ADO_AW_DEFAULT_MODEL_COPILOT} contains invalid characters. Only ASCII alphanumerics, ., _, :, and - are allowed.\" >&2\n\
\x20\x20\x20\x20\x20\x20exit 1\n\
\x20\x20\x20\x20\x20\x20;;\n\

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot taw string preferrred... like the rest of the codebase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d34ee67 by converting the runtime model preamble to a raw string literal.

Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Copilot AI requested a review from jamesadevine September 25, 2026 20:29
@jamesadevine
jamesadevine marked this pull request as ready for review September 26, 2026 01:50
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed the test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ Rust Code Quality Reviewer completed the Rust code quality review.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • releaseassets.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "releaseassets.githubusercontent.com"

See Network Configuration for more information.

🦀 Rust code quality review by Rust Code Quality Reviewer

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ Compiler Contract Reviewer completed the compiler contract review.

🏗️ Compiler contract review by Compiler Contract Reviewer

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Security Reviewer completed the security review.

🔒 Security review by PR Security Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security review: no regressions found

Reviewed the full diff (docs/engine.md, agentic_pipeline.rs, ado_aw_marker.rs, engine.rs) introducing runtime Copilot model pipeline-variable overrides.

  • Runtime model values pass a strict allowlist ([A-Za-z0-9._:-]) in both the bash preamble (engine.rs::runtime_model_preamble) and the aw_info.json writer, before reaching --model "$ADO_AW_EFFECTIVE_MODEL" (quoted) or JSON output — no shell/JSON injection path.
  • The new variable names (ADO_AW_MODEL_AGENT_COPILOT, ADO_AW_MODEL_DETECTION_COPILOT, ADO_AW_DEFAULT_MODEL_COPILOT) are added to BLOCKED_ENV_KEYS, so untrusted workflow engine.env cannot override them.
  • Explicit engine.model still takes precedence over runtime overrides, and no write-capable token or Stage 3 write path is touched.

No exploitable finding. Minor non-security note: the allowlist/precedence logic is duplicated between engine.rs and ado_aw_marker.rs — low risk of the two drifting out of sync over time, not blocking.

🔒 Security review by PR Security Reviewer · auto · 61.6 AIC · ⌖ 5.94 AIC · ⊞ 10.2K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Quality review

The new tests are well-scoped for verifying what strings appear in generated invocations/env/scripts, but they don't verify the runtime shell logic actually behaves correctly — every assertion is a contains() check on generated text, never an execution of the generated bash. Three findings posted inline:

  1. copilot_detection_env's explicit-model omission path is untested (only the Agent-side copilot_env omission has a test), leaving an asymmetric gap.
  2. The runtime-model precedence/validation shell logic (runtime_model_preamble) is only string-matched, never executed, so a broken fallback order or validation regex would pass all four new invocation tests.
  3. The precedence/validation algorithm is independently duplicated between ado_aw_marker.rs's ado_aw_runtime_model (bash) and engine.rs's runtime_model_preamble (bash-generating Rust) with nothing checking they agree.

None of these are merge-blocking on their own — the string-based tests do catch gross omissions — but the actual security-relevant validation logic (rejecting invalid model characters before they reach an embedded shell command) ships with zero behavioral test coverage.

🧪 Test quality analysis by Test Quality Sentinel · auto · 143.7 AIC · ⌖ 6.34 AIC · ⊞ 9.8K
Comment /review to run again

Comment thread src/engine.rs
@@ -928,6 +1005,9 @@ pub fn copilot_detection_env(engine_config: &EngineConfig) -> Result<Vec<(String
pairs.push((key.clone(), value.clone()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copilot_detection_env gates the runtime model vars on engine_config.model().is_none() (line 1005), mirroring copilot_env's gating — but only the Agent-side omission is tested (copilot_engine_env_omits_runtime_agent_model_vars_for_explicit_model). There is no copilot_detection_env test asserting the runtime vars are omitted when an explicit Detection model is configured, so a regression that always injects ADO_AW_MODEL_DETECTION_COPILOT/ADO_AW_DEFAULT_MODEL_COPILOT even with an explicit model would ship silently.

💡 Suggested test
#[test]
fn copilot_detection_env_omits_runtime_model_vars_for_explicit_model() {
    let (front_matter, _) = parse_markdown(
        "---\nname: test\ndescription: test\nsafe-outputs:\n  threat-detection:\n    engine:\n      model: detector-model\n---\n",
    )
    .unwrap();
    let env = copilot_detection_env(&front_matter.engine).unwrap();
    assert!(!env.iter().any(|(key, _)| key == ADO_AW_MODEL_DETECTION_COPILOT));
    assert!(!env.iter().any(|(key, _)| key == ADO_AW_DEFAULT_MODEL_COPILOT));
}

Comment thread src/engine.rs Outdated
&front_matter,
&declarations_for(&front_matter),
"/tmp/threat.md",
None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new runtime-model invocation tests only check that the generated string contains fragments (e.g. invocation.contains("*[!A-Za-z0-9._:-]*)"), invocation.contains("ADO_AW_MODEL_DETECTION_COPILOT")), never that the shell preamble in runtime_model_preamble actually resolves the precedence or rejects invalid characters when executed. This mirrors the implementation rather than testing behaviour — a change that silently breaks the fallback order (specific var, then default var) or the character-validation case guard would pass all four new tests unchanged.

💡 Suggested approach

Extract the generated runtime_model_preamble output to a temp script and execute it via bash -c with different env combinations (unset, specific-var set, default-var set, both set, invalid characters), asserting on $ADO_AW_EFFECTIVE_MODEL / exit status — similar to the pattern already used in tests/compiler_tests.rs::exercise_attached_prompt_with_pinned_copilot_cli for running generated shell.

Comment thread src/compile/extensions/ado_aw_marker.rs Outdated
body: r#"
set -eo pipefail

ado_aw_runtime_model() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ado_aw_runtime_model (bash, EMIT_AW_INFO) reimplements the same precedence/validation algorithm as runtime_model_preamble in src/engine.rs — two independent hand-written copies of the fallback-and-character-validation logic, each only string-matched in tests, with nothing asserting they agree. If one copy's regex/case-guard is tightened or loosened without updating the other, the aw_info.json metadata and the actual --model argument passed to Copilot could silently diverge (e.g. a model rejected by one path but accepted and used by the other).

💡 Suggested approach

Add a small end-to-end test that renders both EMIT_AW_INFO's script and runtime_model_preamble's output for the same ADO_AW_MODEL_AGENT_COPILOT/ADO_AW_DEFAULT_MODEL_COPILOT input combinations (valid, invalid-chars, $(VAR) literal, empty) and checks their outcomes match — or, better, factor the character-validation case pattern into one shared constant/fragment so a change to the allowed character set can't be made in only one place.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust review: COMMENT

Note: the rust-critic sub-agent failed to return output (model access error) on two attempts; findings below are from my own pass only.

Themes
  • One pub method (invocation_with_config) is left as dead code behind #[allow(dead_code)] after this PR rewires its only caller to detection_invocation_with_config.
  • The runtime-model character validation regex/case is duplicated across three locations (shell EMIT_AW_INFO, shell runtime_model_preamble, Rust validate_model_name), risking silent drift.

Both are maintainability concerns, not merge-blockers — the actual injection-prevention logic is sound (validated before use in all three copies), tests pass (cargo test copilot_), and the documented precedence order matches the implementation.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • releaseassets.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "releaseassets.githubusercontent.com"

See Network Configuration for more information.

🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 128.9 AIC · ⌖ 1.96 AIC · ⊞ 10.1K
Comment /review to run again

Comment thread src/engine.rs Outdated
///
/// Retained for callers that need a plain invocation with no role-specific
/// runtime model controls.
#[allow(dead_code)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invocation_with_config is now dead code: after this PR its only caller was replaced by detection_invocation_with_config, and a repo-wide grep shows zero remaining call sites. It is kept alive solely by #[allow(dead_code)] on a pub method, so the compiler warning that would normally catch this is suppressed rather than fixed.

💡 Suggested fix

Either delete invocation_with_config entirely (it duplicates detection_invocation_with_config with runtime_model_role: None, so a caller could just pass None if this ever becomes needed again), or remove the #[allow(dead_code)] and let the compiler flag it if it is truly meant to be temporarily retained. Leaving pub dead code with a suppressed lint is a maintenance trap: a future refactor can silently rot this path without any signal.

Comment thread src/compile/extensions/ado_aw_marker.rs Outdated
body: r#"
set -eo pipefail

ado_aw_runtime_model() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime-model character-class validation (*[!A-Za-z0-9._:-]* in bash) is now implemented three times: here in the EMIT_AW_INFO shell script, again in runtime_model_preamble (src/engine.rs), and as the Rust-side validate_model_name for explicit engine.model. The three copies must stay byte-for-byte in sync or a future edit to one silently diverges from the others, re-opening the injection surface this validation exists to close.

💡 Suggested fix

Extract the bash validation into a single fragment/binding shared by both EMIT_AW_INFO and runtime_model_preamble, or at minimum add a comment cross-referencing all three call sites so a reviewer touching one is prompted to check the others. A regression test asserting the two shell-emitted validation blocks are textually identical would also catch drift automatically.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiler-contract review focused on drift and typed-contract invariants (not general Rust style, covered by other reviewers).

Checked and clean:

  • Front-matter grammar: no new/renamed fields in src/compile/types.rs.
  • BLOCKED_ENV_KEYS correctly extended with the three new ADO_AW_MODEL_* runtime variables, and a test (copilot_engine_env_rejects_user_runtime_model_var_override) proves engine.env cannot shadow them.
  • EMIT_AW_INFO/RUN_AGENT shell changes stay inside the ShellScript/Binding/shell_script! framework — new externals: entries are declared for the three runtime model vars, and ENFORCE_BASH_LINT=1 cargo test --bin ado-aw compile::shell / --test bash_lint_tests both pass locally with the change in place.
  • docs/engine.md documents the new Runtime model controls section, precedence order, and the Detection-engine interaction — matches the code.
  • No public PipelineSummary/GraphSummary fields touched; aw_info.json's model/detection_model keys are optional strings already handled by src/audit/model.rs.
  • No ado-script bundle sources or codegen inputs (filter_ir.rs, Fact) changed, so the bundle/codegen drift checks don't apply here.

One finding posted inline on src/engine.rs: the renamed detection_invocation_with_config orphaned the old invocation_with_config, which is now dead code hidden behind #[allow(dead_code)] instead of being deleted.

🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 200.2 AIC · ⌖ 1.92 AIC · ⊞ 11.3K
Comment /review to run again

Comment thread src/engine.rs Outdated
/// Retained for callers that need a plain invocation with no role-specific
/// runtime model controls.
#[allow(dead_code)]
pub fn invocation_with_config(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invocation_with_config is now unreadable dead code kept alive with #[allow(dead_code)]

Removing the #[allow(dead_code)] and rebuilding produces warning: methods ... invocation_with_config ... never used — the only prior caller was rewired to detection_invocation_with_config in this same PR (src/compile/agentic_pipeline.rs), so this method has zero remaining call sites anywhere in the tree.

💡 Why this matters / fix

Suppressing the warning instead of deleting the method hides genuinely orphaned public API from cargo build/clippy going forward — the next contributor who edits invocation_with_config gets no compiler signal that it is unreachable, and a real future regression in it would go unnoticed since nothing exercises it.

Fix: delete invocation_with_config (and the now-unnecessary #[allow(dead_code)]) unless there is a concrete near-term caller planned; if one is coming in a follow-up PR, say so explicitly instead of the generic "retained for callers that need a plain invocation" note, which as written applies to no one.

Verified via: sed -i 's/#\[allow(dead_code)\]//' src/engine.rs && cargo build --bin ado-aw → emits the never-used warning.

@jamesadevine

Copy link
Copy Markdown
Collaborator

@copilot Address feedback

Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
@github-actions
github-actions Bot dismissed their stale review October 6, 2026 08:28

Superseded by updated review from same workflow.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiler Contract Review

This is a large, well-executed refactor of the Copilot invocation path (shell-string --model/args to a typed JSON request/prepared/result protocol shared by Rust and a new copilot-controller/copilot-runner TS bundle pair). I checked for the contract-drift classes this review focuses on and found none outstanding:

  • Bundle registry / ado_bundle.rs: CopilotController and CopilotRunner are registered in Bundle::ALL, have path entries, and are correctly included in the no-ADO-auth set (GitHub/local-only auth, not ADO REST).
  • Codegen drift: no changes to filter_ir.rs / Fact in this diff, so types.gen.ts / fact-catalog.gen.json correctly did not move.
  • Lock drift: all .github/workflows/*.md changes have matching .lock.yml diffs.
  • AGENTS.md / docs sync: the new copilot-shared/controller/runner directories are documented in the architecture tree and in docs/ado-script.md's bundle list; docs/engine.md gained a full "Runtime model controls" section describing the new ADO_AW_MODEL_* variables and precedence; docs/audit.md documents the new aw_info.json fields (model vs detection_model, requested-vs-observed). All in sync with the code.
  • Generated shell: RUN_AGENT/RUN_THREAT_ANALYSIS route the new trusted-preflight/model-result logic entirely through typed Bindings and a registered phases:/fragments: shell script (APPEND_AW_INFO_FIELD, COPILOT_TRUSTED_PREFLIGHT) -- no format!-built shell, no raw credential bindings.
  • Secure newtypes: the new is_valid_command_path validation (rejecting ./../empty/doubled-slash segments) and validate_model_name are mirrored exactly in protocol.ts's COMMAND_PATTERN/MODEL_PATTERN + hasSafePathSegments, so the Rust and TS halves of this security boundary cannot drift.
  • Prior review feedback: the previously-flagged dead invocation_with_config method and triplicated model-precedence/validation logic are gone in this diff -- functionality was consolidated into invocation_request_with_config / validate_model_name and the shared APPEND_AW_INFO_FIELD phase.

I built the compiler (cargo build), ran the full Rust suite (cargo test, all passing) plus the targeted engine::/validate:: modules and runtime_model_controls_compile_across_all_targets, and ran the new copilot-controller/copilot-runner/copilot-shared vitest suites (43/43 passing) and npm run typecheck (clean). No findings to post inline.

Warning

Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • releaseassets.githubusercontent.com
  • spsprodeus21.vssps.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "releaseassets.githubusercontent.com"
    - "spsprodeus21.vssps.visualstudio.com"

See Network Configuration for more information.

🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 142.1 AIC · ⌖ 1.91 AIC · ⊞ 12.5K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript review — scripts/ado-script/ (new copilot-controller/copilot-runner/copilot-shared protocol)

Reviewed the new copilot invocation split (protocol.ts, copilot-controller/index.ts, copilot-runner/index.ts) plus their tests. ts-critic ran in the background and returned 5 findings; after triage, 2 were genuine and merge-worthy and are posted inline below, the rest (prompt-size guard, swallowed JSON.parse detail, broad argv-path trust) are advisory nitpicks not worth blocking on.

Strengths:

  • The request/prepared/result schema validation in protocol.ts is thorough — unknown-key rejection, NUL-byte checks, path/command allowlisting via regex, and no any/non-null-assertion type-safety holes on externally parsed JSON.
  • Signal forwarding and exit-code mapping in copilot-runner is carefully handled (settle guard against double-resolve, off cleanup of listeners).
  • Good test coverage of model precedence (resolveRequestedModel), unresolved ADO macro detection, and the role-mismatch / missing-result paths in copilot-controller.

Findings (inline):

  1. copilot-controller/index.ts:27 — write-ordering bug: result.json is written before prepared.json, which can leave a stale/misleading result visible to the trusted host after a crash between the two writes, contradicting the documented "commit result before exposing prepared" intent.
  2. copilot-runner/index.ts:50 — self-removal of preparedPath/runnerPath happens before spawn is confirmed to succeed, so a spawn error event (binary missing, perms) loses the invocation audit trail that would otherwise help debug the failure.

Neither is merge-blocking on its own (both are narrow-window failure modes), but #1 in particular undermines the stated trust-boundary guarantee of the two-file protocol, so I'd like it addressed or the design rationale clarified before merge.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 154.9 AIC · ⌖ 8.49 AIC · ⊞ 11.3K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security review — no blocking findings

Reviewed the diff against the threat model (compiler → generated-YAML injection, Stage 1/3 token isolation, AWF sandbox boundary, validation strength).

Net assessment: security-neutral to positive. The headline change — replacing the shell-embedded --prompt="$(cat ...)" engine invocation string with a copilot-controller/copilot-runner protocol that spawns Copilot via child_process.spawn(command, argv[]) — removes a shell-reinterpretation step for prompt/arg content and is a hardening, not a regression.

Specifics checked:

  • is_valid_command_path (Rust) and isSafeCommand/hasSafePathSegments (TS protocol.ts) are tightened together: both now reject ./.. segments, empty segments, and trailing slashes — stricter than the prior character-class-only check.
  • Bundle::CopilotController / Bundle::CopilotRunner are correctly placed in the no-SYSTEM_ACCESSTOKEN bucket in ado_bundle.rs (no ADO REST calls), preserving least privilege.
  • The trusted controller/result files live under $(Agent.TempDirectory), outside AWF's automatic /tmp mount (per docs/network.md), matching the existing azure-wif-refresh isolation pattern. The controller source copied into the sandbox-visible path is deleted before AWF starts, and AWF hasn't executed yet at that point, so there's no attacker-controlled race window.
  • ADO_AW_MODEL_AGENT_COPILOT / ADO_AW_MODEL_DETECTION_COPILOT / ADO_AW_DEFAULT_MODEL_COPILOT / COPILOT_MODEL were added to BLOCKED_ENV_KEYS, and a new check rejects --model in engine.args, closing the obvious bypass of the new precedence chain. buildChildEnvironment strips the runtime-override vars before spawning the real Copilot process.
  • validate_model_name / MODEL_PATTERN are kept in sync between the Rust compiler and the TS protocol module.

Didn't find a traceable injection, token-exposure, or weakened-validation path introduced by this diff, so no inline comments. (Pre-existing review threads already cover the DRY/dead-code concerns in engine.rs and ado_aw_marker.rs — out of scope for a security pass.)

🔒 Security review by PR Security Reviewer · auto · 157.6 AIC · ⌖ 2.72 AIC · ⊞ 11.4K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: COMMENT — no new merge-blocking defects in the Rust changes

Reviewed the Rust-only portion of the diff (src/audit/**, src/compile/ado_bundle.rs, src/compile/agentic_pipeline.rs, src/compile/extensions/{ado_aw_marker,ado_script,exec_context,mod}.rs, src/compile/types.rs, src/engine.rs, src/validate.rs, tests/compiler_tests.rs).

rust-critic returned three findings; I verified each:

  • HIGH — "untrusted model name interpolated into JSON via printf without escaping" (ado_aw_marker.rs): verified false positive. REQUESTED_MODEL is validated against ^[A-Za-z0-9._:-]+$ (validate_model_name in src/engine.rs, mirrored by MODEL_PATTERN in scripts/ado-script/src/copilot-shared/protocol.ts) both when the request is built and again when parseInvocationResult re-parses the controller's result file. A quote or backslash can never reach the printf call.
  • LOW — blocking Path::is_file() inside async find_analyzed_outputs_dir: this mirrors the pre-existing identical pattern in otel.rs::locate_agent_output_file (not introduced by this PR) — consistent with current codebase convention, not a regression.
  • LOW — is_valid_command_path has no explicit length cap: defense-in-depth suggestion only; no demonstrated exploit path since the character allowlist already excludes shell metacharacters. Not blocking.

My own pass over error handling, casts, cross-platform paths, determinism (HashMap/sort() usage in copilot_provider_env/copilot_detection_env), and the new set -eo pipefail / set +e exit-code plumbing in RUN_AGENT/RUN_THREAT_ANALYSIS didn't surface additional issues — the fail-closed ordering (trusted prepare runs under set -e before set +e) is correct and is exercised by the new tests. cargo build is clean.

The genuinely interesting architectural risks here — triplicated runtime-model precedence/validation logic across engine.rs, the EMIT_AW_INFO bash script, and the TS protocol, the copilot_detection_env/copilot_engine_env test-coverage asymmetry, and the invocation_with_config dead-code path — are already tracked in existing review threads on this PR, so I'm not duplicating them.

🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 401.6 AIC · ⌖ 2.27 AIC · ⊞ 11.3K
Comment /review to run again

jamesadevine and others added 2 commits October 6, 2026 10:02
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
@jamesadevine

Copy link
Copy Markdown
Collaborator

/azp run ado-aw candidate compiler smoke

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
@jamesadevine

Copy link
Copy Markdown
Collaborator

/azp run ado-aw candidate compiler smoke

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
@jamesadevine

Copy link
Copy Markdown
Collaborator

/azp run ado-aw candidate compiler smoke

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
@jamesadevine

Copy link
Copy Markdown
Collaborator

/azp run ado-aw candidate compiler smoke

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
@jamesadevine

Copy link
Copy Markdown
Collaborator

/azp run ado-aw candidate compiler smoke

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@jamesadevine

Copy link
Copy Markdown
Collaborator

/azp run ado-aw candidate compiler smoke

@jamesadevine

Copy link
Copy Markdown
Collaborator

/review

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Rust Code Quality Reviewer failed during the Rust code quality review.

🦀 Rust code quality review by Rust Code Quality Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiler-contract review

Focused on drift (codegen/lock/smoke), front-matter/safe-output/IR schema contracts, generated-shell discipline, and docs sync. Did not duplicate the Rust/TS quality findings already in pr-review-comments.json (the copilot_detection_env/copilot_env gating-test gap, the triplicated validation-algorithm note, and the shallow contains() invocation tests — all still accurate as of this diff and worth addressing).

No new contract-drift findings. Specifically checked and clean:

  • Codegen/lock/smoke drift: no filter_ir.rs/Fact changes in this diff, so types.gen.ts/fact-catalog.gen.json correctly didn't move. No .github/workflows/*.md changed without its .lock.yml. tests/smoke/runtime-model-*.md are markdown-only sources wired through one cases.json entry each (REGISTERED.md updated to explain the agentic-lane queue-time variable) — matches the lane/registration design, no reintroduced per-case ADO definitions.
  • Front-matter grammar: no new/removed FrontMatter fields in types.rs (only two internal helper methods removed, both now dead after the exec_context_*_active/safe_outputs_summary_active/etc. flag removal) — no codemod needed.
  • Duplicated-activation-predicate removal: this PR actually resolves a drift risk rather than introducing one — the pr_contributor_will_activate/manual_contributor_will_activate/etc. "MUST stay in lock-step" helper pairs between exec_context/mod.rs and AdoScriptExtension are deleted wholesale, replaced by always-staging the Copilot controller/runner bundles in Agent prepare. Good simplification.
  • New Bundle::CopilotController/CopilotRunner: correctly added to ado_bundle.rs's "no ADO bearer" exemption list (GitHub/no-REST bundles) — consistent with their GitHub App / credential-free nature.
  • Generated shell: RUN_AGENT/RUN_THREAT_ANALYSIS/RECORD_DETECTION_RUNTIME_MODEL all go through ShellScript with typed bindings; the JSON invocation request is correctly passed via Binding::document (not raw interpolation), and the shared APPEND_AW_INFO_FIELD phase is reused via phase_body() rather than re-implemented at each call site — avoids yet another drift copy.
  • Docs sync: AGENTS.md architecture tree, docs/ado-script.md, docs/engine.md (new "Runtime model controls" section), and docs/audit.md (new aw_info.model/detection_model semantics + overlay behavior) are all updated and match the implementation (src/audit/model.rs, cli.rs's merge_detection_aw_info, otel.rs's observed-vs-requested model precedence) — verified with the corresponding new tests in each file.
  • Trusted/untrusted boundary: the new copilot-controller/copilot-runner/copilot-shared split (prepare on host, run-only in sandbox, strict schema-v2 validation with rejectUnknown, atomic writes, self-removal of the prepared doc before exec) matches the AGENTS.md security note added in this same PR about never trusting /tmp artifacts across the AWF boundary — the controller copy and result live under $(Agent.TempDirectory), outside AWF's host /tmp mount, consistent with that rule.

Nothing here rises to REQUEST_CHANGES on compiler-contract grounds; the gating/test-coverage gaps already flagged by prior reviews remain the operative action items.

🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 129.7 AIC · ⌖ 18.1 AIC · ⊞ 12.5K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security review: no regressions found

This diff is security-neutral to positive. The headline change — replacing the shell-interpolated --prompt="$(cat path)" invocation (embedded inside a single-quoted AWF -- command string) with a trusted copilot-controller/copilot-runner protocol that reads the prompt file and passes it as a literal spawn() argv element (no shell: true) — removes a shell-expansion surface for prompt/model content rather than introducing one.

Specific checks performed against the threat model in this repo's docs:

  • Injection into generated pipelines: the new INVOCATION_REQUEST document is built via serde_json::to_string and embedded through the existing Binding::document heredoc binding (which asserts no credential content and no delimiter collision) — not string concatenation. command, prompt_path, and mcp_config_path are compiler-controlled, not operator/PR-controlled.
  • Weakened validation: none found. is_valid_command_path (src/validate.rs) got stricter (now rejects .., empty segments, trailing slash, bare relative paths like bin/copilot), and a new validate_model_name / MODEL_PATTERN enforces ^[A-Za-z0-9._:-]+$ consistently in both src/engine.rs and scripts/ado-script/src/copilot-shared/protocol.ts. protocol.ts additionally rejects unknown JSON fields (rejectUnknown) and NUL bytes in every parsed document.
  • Token scope / ado_bundle.rs: CopilotController/CopilotRunner are added to the bundle registry and correctly placed alongside the no-ADO-auth bundles (GithubAppToken, ApprovalSummary, etc.) rather than the SYSTEM_ACCESSTOKEN-projected set — consistent with them never making ADO REST calls.
  • Trust boundary: the controller (trusted, runs outside AWF, install -m 0500) writes the sandbox-visible invocation document and is then deleted (rm -f "$COPILOT_CONTROLLER_SOURCE_PATH") before AWF starts; the runner (untrusted, runs inside AWF) only reads the prepared document, which has already been schema-validated and had the compiler-forbidden ADO_AW_MODEL_*/COPILOT_MODEL env keys stripped via buildChildEnvironment. Prepared files are atomically written (writeJsonAtomic, mode 0600, rename-after-write).
  • Network/allowlist: no new domains added.
  • Safe-output integrity: unaffected by this diff.

No merge-blocking findings. The existing unresolved github-actions[bot] review threads (duplicated validation logic across runtime_model_preamble/ado_aw_runtime_model/protocol.ts, and the invocation_with_config dead-code question) are code-quality/maintainability concerns, not security regressions, and are already tracked — not duplicating them here.

🔒 Security review by PR Security Reviewer · auto · 115.8 AIC · ⌖ 2.02 AIC · ⊞ 11.4K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Quality Review

This PR's test coverage is thorough. The new copilot-shared/protocol.ts module is exhaustively tested (strict schema validation, every rejection path, atomic-write behavior, argv/env construction including adversarial prompt content). copilot-controller/copilot-runner each cover their success, error, and signal-forwarding paths, including the security-relevant "does not expose sandbox run mode" / "does not expose controller modes" boundary tests. The engine.rs runtime-model precedence and gating logic — previously flagged in earlier review passes as having an untested Detection-side gap and weak substring-only invocation assertions — now has symmetric Agent/Detection tests with structured field assertions (copilot_detection_env_omits_runtime_model_vars_for_explicit_model, copilot_invocation_request_defers_runtime_model_resolution, etc.), and the previously-flagged invocation_with_config dead code no longer exists. The Rust ado_aw_marker.rs metadata-merge logic is well covered for value/key-collision edge cases (e.g. append_aw_info_field_does_not_confuse_value_with_model_key) and failure-without-exit-propagation. The new smoke cases follow the two-file convention correctly.

I found one minor untested branch (inline comment): the audit console renderer's new requested-vs-observed model split only has tests for the "differ" and "equal" branches, not the "observed model absent" branch described as a non-fatal case in the PR summary.

Non-blocking — approving the test quality here.

🧪 Test quality analysis by Test Quality Sentinel · auto · 213.9 AIC · ⌖ 1.7 AIC · ⊞ 11K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript review (scripts/ado-script/)

Reviewed the new Copilot invocation protocol (copilot-shared/protocol.ts, copilot-controller, copilot-runner) and the compiler-smoke-e2e queue-variable/model-assertion additions.

No high-confidence defects found:

  • Error handling is sound: copilot-runner forwards SIGINT/SIGTERM/SIGHUP with proper cleanup, settles exactly once, and fails closed when self-removal throws.
  • resolveRequestedModel validates runtime model values against a strict character class and never includes the rejected value in its thrown message (verified by test).
  • Path/command validation (hasSafePathSegments, isSafeCommand) correctly rejects traversal segments, relative paths, and trailing slashes.
  • writeJsonAtomic writes with 0o600 and renames, avoiding partial-write exposure.
  • New branches in signals.ts/cases.ts/ado-rest.ts are covered by matching test cases.

A second pass via a dedicated hostile-review sub-agent independently reached the same conclusion (no findings).

No inline comments to post.

🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 235.3 AIC · ⌖ 2.62 AIC · ⊞ 11.3K
Comment /review to run again

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[agent-issue]: Mirror gh-aw's runtime model controls in ado-aw

2 participants